#!/usr/bin/env python
# -*- coding: UTF-8 -*-
'''
@Project ：V2 
@File    ：TCplex.py
@IDE     ：PyCharm 
@Author  ：郭星
@Date    ：2025/9/15 21:46
求解器球技巧
'''
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


"""
求解器球技巧
"""
"""
MIP 模型（机器可选 + 工序先后 + 同机不可重叠（加序约束）+ 最小化完工期 Cmax）

读取/构造实例、求解、还原排程

输出最优/可行完工期并绘制甘特图

依赖：pip install docplex matplotlib
需要本地安装 IBM CPLEX（社区版即可）或配置 DOcplex 云服务。
运行：python cplex_fjsp.py
建模要点

x[j,k,m]：操作 (j,k) 选择机器 m 的二元变量

s[j,k]：开始时间

机台序约束用成对顺序二元变量 z + 条件化的大 M（只有当两操作都被分配到同一机器时才生效）

目标 min Cmax，并对每个操作加 s + Σ p*m*x ≤ Cmax

大 M 的选择
这里用每道工序在可行机器上的最大加工时间求和作为上界，足够安全；若想加强模型，可用更紧的上界（比如基于拓扑和机器并行度推得的 upper bound）。

规模建议
FJSP 是 NP-hard，配对序变量数随规模快速增长。建议：

先用小实例调通；

较大实例加 timelimit、启发式初解、或转为 CP 优化（docplex.cp）/元启发式（你之前的 GA/ABC/FOA/GWO/DQN/GP）。

复用
你可以把 build_fjsp_model() 直接导入其他工程，只需提供 FJSPInstance；或把 tiny_demo_instance() 替换为你自己的数据读入（JSON/CSV）。
CPLEX (docplex.mp) exact MIP for Flexible Job Shop Scheduling (FJSP)
- Binary assignment: each operation chooses exactly one eligible machine
- Precedence: within each job, op(k+1) starts after op(k) completes
- Disjunctive sequencing: on any machine, operations cannot overlap (pairwise order with big-M)
- Objective: minimize makespan (Cmax)
- Output: best found makespan and Gantt chart

Requires:
  pip install docplex matplotlib
And IBM CPLEX installed (Community Edition is OK).

Author: you :)
"""

from dataclasses import dataclass
from typing import Dict, List, Tuple
import math
import itertools
import matplotlib.pyplot as plt

try:
    from docplex.mp.model import Model
except ImportError:
    raise SystemExit("Please install docplex: pip install docplex")

# ----------------------------
# Data structures
# ----------------------------

@dataclass
class Operation:
    # feasible machines and processing times
    # keys are machine ids in [0..m-1]
    machine_time: Dict[int, float]

@dataclass
class Job:
    ops: List[Operation]

@dataclass
class FJSPInstance:
    jobs: List[Job]
    m: int  # number of machines

    @property
    def n_jobs(self) -> int:
        return len(self.jobs)

    @property
    def ops_list(self) -> List[Tuple[int, int]]:
        """List of operation indices as tuples (j, k)"""
        lst = []
        for j, job in enumerate(self.jobs):
            for k in range(len(job.ops)):
                lst.append((j, k))
        return lst

    @property
    def n_ops(self) -> int:
        return sum(len(job.ops) for job in self.jobs)

    def eligible(self, j: int, k: int) -> List[int]:
        return list(self.jobs[j].ops[k].machine_time.keys())

    def p(self, j: int, k: int, m: int) -> float:
        return float(self.jobs[j].ops[k].machine_time[m])

    def bigM(self) -> float:
        """A safe upper bound for time (used as Big-M)."""
        # sum of max processing time of each op (coarse but safe)
        return sum(max(op.machine_time.values()) for job in self.jobs for op in job.ops) + 1.0

# ----------------------------
# Model builder
# ----------------------------

def build_fjsp_model(ins: FJSPInstance, timelimit: float = None):
    mdl = Model(name="FJSP_CPLEX")

    J = ins.n_jobs
    M = ins.m
    OPS = ins.ops_list  # list of (j,k)
    BIGM = ins.bigM()

    # Decision variables
    # x[j,k,m] = 1 if op (j,k) is assigned to machine m
    x = {(j,k,m): mdl.binary_var(name=f"x_{j}_{k}_{m}")
         for (j,k) in OPS for m in ins.eligible(j,k)}

    # s[j,k] start times
    s = {(j,k): mdl.continuous_var(lb=0.0, name=f"s_{j}_{k}") for (j,k) in OPS}

    # Cmax
    Cmax = mdl.continuous_var(lb=0.0, name="Cmax")

    # Sequencing variables on machines:
    # for each machine m, for every pair of operations (o1,o2) that both can be processed on m,
    # z[o1,o2,m] = 1 if o1 precedes o2 on machine m (only one direction will be enforced if both assigned to m)
    z = {}
    for m in range(M):
        # operations eligible on m
        ops_m = [(j,k) for (j,k) in OPS if m in ins.eligible(j,k)]
        for (j1,k1), (j2,k2) in itertools.combinations(ops_m, 2):
            z[(j1,k1,j2,k2,m)] = mdl.binary_var(name=f"z_{j1}_{k1}_{j2}_{k2}_m{m}")

    # -------------------
    # Constraints
    # -------------------

    # 1) Assignment: each operation chooses exactly one machine
    for (j,k) in OPS:
        mdl.add(mdl.sum(x[(j,k,m)] for m in ins.eligible(j,k)) == 1, ctname=f"assign_{j}_{k}")

    # 2) Job precedence: op k+1 starts after op k completes
    for j in range(J):
        for k in range(len(ins.jobs[j].ops)-1):
            # s[j,k+1] >= s[j,k] + sum_m p[j,k,m] * x[j,k,m]
            mdl.add(
                s[(j,k+1)] >= s[(j,k)] + mdl.sum(ins.p(j,k,m) * x[(j,k,m)] for m in ins.eligible(j,k)),
                ctname=f"prec_{j}_{k}_to_{k+1}"
            )

    # 3) Machine capacity / disjunctive sequencing:
    # For any m and any pair (o1=(j1,k1), o2=(j2,k2)) eligible on m:
    # s[o1] + p[o1,m] <= s[o2] + BIGM*(1 - z[o1,o2,m]) + BIGM*(2 - x[o1,m] - x[o2,m])
    # s[o2] + p[o2,m] <= s[o1] + BIGM*(    z[o1,o2,m]) + BIGM*(2 - x[o1,m] - x[o2,m])
    # If both assigned to m, exactly one order will be enforced by small BIGM terms above.
    for m in range(M):
        ops_m = [(j,k) for (j,k) in OPS if m in ins.eligible(j,k)]
        for (j1,k1), (j2,k2) in itertools.combinations(ops_m, 2):
            zvar = z[(j1,k1,j2,k2,m)]
            # o1 before o2
            mdl.add(
                s[(j1,k1)] + ins.p(j1,k1,m)
                <= s[(j2,k2)] + BIGM*(1 - zvar) + BIGM*(2 - x[(j1,k1,m)] - x[(j2,k2,m)]),
                ctname=f"seq1_{j1}_{k1}_{j2}_{k2}_m{m}"
            )
            # o2 before o1
            mdl.add(
                s[(j2,k2)] + ins.p(j2,k2,m)
                <= s[(j1,k1)] + BIGM*(zvar) + BIGM*(2 - x[(j1,k1,m)] - x[(j2,k2,m)]),
                ctname=f"seq2_{j1}_{k1}_{j2}_{k2}_m{m}"
            )

    # 4) Cmax: completion time of every operation <= Cmax
    for (j,k) in OPS:
        mdl.add(
            s[(j,k)] + mdl.sum(ins.p(j,k,m) * x[(j,k,m)] for m in ins.eligible(j,k)) <= Cmax,
            ctname=f"cmax_{j}_{k}"
        )

    # Optional upper bound on start times (tighten)
    for (j,k) in OPS:
        mdl.add(s[(j,k)] <= BIGM, ctname=f"ubS_{j}_{k}")

    # Objective
    mdl.minimize(Cmax)

    # Parameters (optional)
    if timelimit is not None:
        mdl.parameters.timelimit = float(timelimit)
    mdl.parameters.mip.tolerances.mipgap = 1e-4  # tighten gap if you like

    return mdl, x, s, Cmax

# ----------------------------
# Helpers: solution extraction & plotting
# ----------------------------

def extract_schedule(ins: FJSPInstance, x, s) -> Tuple[float, Dict[Tuple[int,int], Tuple[int, float, float]]]:
    """Return makespan and assign dict[(j,k)] = (m, start, end)"""
    assign = {}
    makespan = 0.0
    for j in range(ins.n_jobs):
        for k, op in enumerate(ins.jobs[j].ops):
            # which machine chosen?
            chosen_m = None
            for m in op.machine_time.keys():
                var = x[(j,k,m)]
                if var.solution_value > 0.5:
                    chosen_m = m
                    break
            if chosen_m is None:
                # not assigned -> infeasible solution
                continue
            start = s[(j,k)].solution_value
            end = start + ins.p(j,k,chosen_m)
            assign[(j,k)] = (chosen_m, start, end)
            makespan = max(makespan, end)
    return makespan, assign

def plot_gantt_fjsp(ins: FJSPInstance, assign: Dict[Tuple[int,int], Tuple[int, float, float]], title: str):
    bars = {m: [] for m in range(ins.m)}
    for (j,k), (m,s,e) in assign.items():
        bars[m].append((s, e-s, f"J{j}-O{k}"))
    fig, ax = plt.subplots(figsize=(12, 0.7*ins.m + 3))
    yticks, ylabels = [], []
    for row, m in enumerate(sorted(bars)):
        items = sorted(bars[m], key=lambda x: x[0])
        for (start, dur, label) in items:
            ax.barh(row, dur, left=start)
            ax.text(start + 0.5*dur, row, label, ha="center", va="center", fontsize=8)
        yticks.append(row); ylabels.append(f"M{m}")
    ax.set_yticks(yticks); ax.set_yticklabels(ylabels)
    ax.set_xlabel("Time"); ax.set_title(title)
    ax.grid(axis="x", alpha=0.3)
    plt.tight_layout(); plt.show()

# ----------------------------
# Example instance
# ----------------------------

def tiny_demo_instance() -> FJSPInstance:
    """
    A small FJSP instance:
    - 4 jobs, each with 3-4 operations
    - 5 machines (0..4)
    Times are small to solve quickly even without a long timelimit.
    """
    jobs: List[Job] = []

    # Job 0: 3 ops
    jobs.append(Job(ops=[
        Operation({0:4, 1:6}),         # O0,0
        Operation({1:5, 3:3, 4:4}),    # O0,1
        Operation({0:6, 2:5})          # O0,2
    ]))

    # Job 1: 4 ops
    jobs.append(Job(ops=[
        Operation({2:3, 3:5}),
        Operation({0:4, 4:6}),
        Operation({1:3, 2:4, 3:5}),
        Operation({0:2, 2:3})
    ]))

    # Job 2: 3 ops
    jobs.append(Job(ops=[
        Operation({0:5, 1:4}),
        Operation({2:6, 4:4}),
        Operation({1:5, 3:4})
    ]))

    # Job 3: 3 ops
    jobs.append(Job(ops=[
        Operation({3:3, 4:5}),
        Operation({0:5, 2:4}),
        Operation({1:4, 4:3})
    ]))

    return FJSPInstance(jobs=jobs, m=5)

# ----------------------------
# Main
# ----------------------------

def main():
    ins = tiny_demo_instance()
    mdl, x, s, Cmax = build_fjsp_model(ins, timelimit=60)  # set a small time limit if needed

    print("Solving...")
    sol = mdl.solve(log_output=True)
    if sol is None:
        print("No solution found (infeasible or time limit hit without incumbent).")
        return

    mk, assign = extract_schedule(ins, x, s)
    print(f"\nBest found makespan (Cmax): {mk:.2f}")

    # Print assignment (optional)
    for (j,k), (m, st, en) in sorted(assign.items()):
        print(f"Job {j} Op {k} -> M{m}  start={st:.1f} end={en:.1f}")

    # Gantt
    plot_gantt_fjsp(ins, assign, title=f"FJSP – CPLEX schedule (Cmax={mk:.1f})")

if __name__ == "__main__":
    main()
